package com.example.hadoopdemo.executor.recommend.job;

import org.apache.mahout.cf.taste.common.Refreshable;
import org.apache.mahout.cf.taste.common.TasteException;
import org.apache.mahout.cf.taste.eval.DataModelBuilder;
import org.apache.mahout.cf.taste.eval.IRStatistics;
import org.apache.mahout.cf.taste.eval.RecommenderBuilder;
import org.apache.mahout.cf.taste.eval.RecommenderEvaluator;
import org.apache.mahout.cf.taste.eval.RecommenderIRStatsEvaluator;
import org.apache.mahout.cf.taste.impl.common.FastByIDMap;
import org.apache.mahout.cf.taste.impl.eval.AverageAbsoluteDifferenceRecommenderEvaluator;
import org.apache.mahout.cf.taste.impl.eval.GenericRecommenderIRStatsEvaluator;
import org.apache.mahout.cf.taste.impl.eval.RMSRecommenderEvaluator;
import org.apache.mahout.cf.taste.impl.model.GenericBooleanPrefDataModel;
import org.apache.mahout.cf.taste.impl.model.file.FileDataModel;
import org.apache.mahout.cf.taste.impl.neighborhood.NearestNUserNeighborhood;
import org.apache.mahout.cf.taste.impl.neighborhood.ThresholdUserNeighborhood;
import org.apache.mahout.cf.taste.impl.recommender.*;
import org.apache.mahout.cf.taste.impl.recommender.knn.KnnItemBasedRecommender;
import org.apache.mahout.cf.taste.impl.recommender.knn.Optimizer;
import org.apache.mahout.cf.taste.impl.recommender.slopeone.SlopeOneRecommender;
import org.apache.mahout.cf.taste.impl.recommender.svd.Factorizer;
import org.apache.mahout.cf.taste.impl.recommender.svd.SVDRecommender;
import org.apache.mahout.cf.taste.impl.similarity.*;
import org.apache.mahout.cf.taste.model.DataModel;
import org.apache.mahout.cf.taste.model.PreferenceArray;
import org.apache.mahout.cf.taste.neighborhood.UserNeighborhood;
import org.apache.mahout.cf.taste.recommender.RecommendedItem;
import org.apache.mahout.cf.taste.recommender.Recommender;
import org.apache.mahout.cf.taste.similarity.ItemSimilarity;
import org.apache.mahout.cf.taste.similarity.UserSimilarity;

import java.io.File;
import java.io.IOException;
import java.util.List;

/**
 * @author Ruison
 * @date 2022/1/25
 */
public class RecommendFactory {

    /**
     * 构建通用数据模型
     *
     * @param file 文件路径地址
     * @return {@link DataModel}
     * @throws TasteException
     * @throws IOException
     */
    public static DataModel buildDataModel(String file) throws TasteException, IOException {
        return new FileDataModel(new File(file));
    }

    /**
     * 构建通用数据模型（不建议在需要性能高的场景下使用）
     *
     * @param file 文件路径地址
     * @return {@link DataModel}
     * @throws TasteException 异常
     * @throws IOException    异常
     */
    public static DataModel buildDataModelNoPref(String file) throws TasteException, IOException {
        return new GenericBooleanPrefDataModel(GenericBooleanPrefDataModel.toDataMap(new FileDataModel(new File(file))));
    }

    /**
     * 通用数据模型构建器
     *
     * @return {@link DataModelBuilder}
     */
    public static DataModelBuilder buildDataModelNoPrefBuilder() {
        return new DataModelBuilder() {
            @Override
            public DataModel buildDataModel(FastByIDMap<PreferenceArray> trainingData) {
                return new GenericBooleanPrefDataModel(GenericBooleanPrefDataModel.toDataMap(trainingData));
            }
        };
    }

    /**
     * 相似枚举
     */
    public enum SIMILARITY {
        PEARSON, EUCLIDEAN, COSINE, TANIMOTO, LOGLIKELIHOOD, SPEARMAN, CITYBLOCK, FARTHEST_NEIGHBOR_CLUSTER, NEAREST_NEIGHBOR_CLUSTER
    }

    /**
     * 两个用户之间相似度算法接口
     *
     * @param type  相似度算法类型
     * @param model {@link DataModel} 数据模型
     * @return {@link UserSimilarity}
     * @throws TasteException Taste 引擎抛出的异常
     */
    public static UserSimilarity userSimilarity(SIMILARITY type, DataModel model) throws TasteException {
        switch (type) {
            /*
                皮尔逊相关系数相似度算法
                原理：用来反映两个变量线性相关程度的统计量
                范围：[-1,1]，绝对值越大，说明相关性越强，负相关对于推荐的意义小。
                说明：1、 不考虑重叠的数量；
                     2、 如果只有一项重叠，无法计算相似性（计算过程被除数有n-1）；
                     3、 如果重叠的值都相等，也无法计算相似性（标准差为0，做除数）。
                注意：该相似度并不是最好的选择，也不是最坏的选择，只是因为其容易理解，在早期研究中经常被提起。
                使用Pearson线性相关系数必须假设数据是成对地从正态分布中取得的，并且数据至少在逻辑范畴内必须是等间距的数据。
                Mahout中，为皮尔森相关计算提供了一个扩展，通过增加一个枚举类型（Weighting）的参数来使得重叠数也成为计算相似度的影响因子。
             */
            case PEARSON:
                return new PearsonCorrelationSimilarity(model);
            /*
                余弦相似度算法
                原理：多维空间两点与所设定的点形成夹角的余弦值。
                范围：[-1,1]，值越大，说明夹角越大，两点相距就越远，相似度就越小。
                说明：在数学表达中，如果对两个项的属性进行了数据中心化，计算出来的余弦相似度和皮尔森相似度是一样的。
                在mahout中，实现了数据中心化的过程，所以皮尔森相似度值也是数据中心化后的余弦相似度。
                另外在新版本中，Mahout提供了 UncenteredCosineSimilarity 类作为计算非中心化数据的余弦相似度。
             */
            case COSINE:
                return new UncenteredCosineSimilarity(model);
            /*
                Tanimoto 系数相似度算法
                原理：又名广义 Jaccard 系数，是对 Jaccard 系数的扩展。
                范围：[0,1]，完全重叠时为1，无重叠项时为0，越接近1说明越相似。
                说明：处理无打分的偏好数据
             */
            case TANIMOTO:
                return new TanimotoCoefficientSimilarity(model);
            /*
                对数似然相似度算法
                原理：重叠的个数，不重叠的个数，都没有的个数
                范围：具体可去百度文库中查找论文《Accurate Methods for the Statistics of Surprise and Coincidence》
                说明：处理无打分的偏好数据，比 Tanimoto 系数的计算方法更为智能。
             */
            case LOGLIKELIHOOD:
                return new LogLikelihoodSimilarity(model);
            /*
                Spearman 秩相关系数相似度度算法
                原理：Spearman秩相关系数通常被认为是排列后的变量之间的Pearson线性相关系数。
                范围：{-1.0,1.0}，当一致时为1.0，不一致时为-1.0。
                说明：计算非常慢，有大量排序。针对推荐系统中的数据集来讲，用 Spearman 秩相关系数作为相似度量是不合适的。
             */
            case SPEARMAN:
                return new SpearmanCorrelationSimilarity(model);
            /*
                曼哈顿距离相似度算法
                原理：曼哈顿距离的实现，同欧式距离相似，都是用于多维数据空间距离的测度
                范围：[0,1]，同欧式距离一致，值越小，说明距离值越大，相似度越大。
                说明：比欧式距离计算量少，性能相对高。
             */
            case CITYBLOCK:
                return new CityBlockSimilarity(model);
            /*
                欧式距离相似度算法
                原理：利用欧式距离d定义的相似度s，s=1 / (1+d)。
                说明：同皮尔森相似度一样，该相似度也没有考虑重叠数对结果的影响。同样，Mahout通过增加一个枚举类型（Weighting）的参数来使得重叠数也成为计算相似度的影响因子。
             */
            case EUCLIDEAN:
            default:
                return new EuclideanDistanceSimilarity(model);
        }
    }

    /**
     * 两个项目之间相似度算法接口
     *
     * @param type  {@link SIMILARITY}相似度算法类型
     * @param model {@link DataModel} 数据模型
     * @return {@link ItemSimilarity}
     * @throws TasteException Taste 引擎抛出的异常
     */
    public static ItemSimilarity itemSimilarity(SIMILARITY type, DataModel model) throws TasteException {
        switch (type) {
            /*
                皮尔逊相关系数相似度算法
                原理：用来反映两个变量线性相关程度的统计量
                范围：[-1,1]，绝对值越大，说明相关性越强，负相关对于推荐的意义小。
                说明：1、 不考虑重叠的数量；
                     2、 如果只有一项重叠，无法计算相似性（计算过程被除数有n-1）；
                     3、 如果重叠的值都相等，也无法计算相似性（标准差为0，做除数）。
                注意：该相似度并不是最好的选择，也不是最坏的选择，只是因为其容易理解，在早期研究中经常被提起。
                使用Pearson线性相关系数必须假设数据是成对地从正态分布中取得的，并且数据至少在逻辑范畴内必须是等间距的数据。
                Mahout中，为皮尔森相关计算提供了一个扩展，通过增加一个枚举类型（Weighting）的参数来使得重叠数也成为计算相似度的影响因子。
             */
            case PEARSON:
                return new PearsonCorrelationSimilarity(model);
            /*
                余弦相似度算法
                原理：多维空间两点与所设定的点形成夹角的余弦值。
                范围：[-1,1]，值越大，说明夹角越大，两点相距就越远，相似度就越小。
                说明：在数学表达中，如果对两个项的属性进行了数据中心化，计算出来的余弦相似度和皮尔森相似度是一样的。
                在mahout中，实现了数据中心化的过程，所以皮尔森相似度值也是数据中心化后的余弦相似度。
                另外在新版本中，Mahout提供了 UncenteredCosineSimilarity 类作为计算非中心化数据的余弦相似度。
             */
            case COSINE:
                return new UncenteredCosineSimilarity(model);
            /*
                Tanimoto 系数相似度算法
                原理：又名广义 Jaccard 系数，是对 Jaccard 系数的扩展。
                范围：[0,1]，完全重叠时为1，无重叠项时为0，越接近1说明越相似。
                说明：处理无打分的偏好数据
             */
            case TANIMOTO:
                return new TanimotoCoefficientSimilarity(model);
            /*
                对数似然相似度算法
                原理：重叠的个数，不重叠的个数，都没有的个数
                范围：具体可去百度文库中查找论文《Accurate Methods for the Statistics of Surprise and Coincidence》
                说明：处理无打分的偏好数据，比 Tanimoto 系数的计算方法更为智能。
             */
            case LOGLIKELIHOOD:
                return new LogLikelihoodSimilarity(model);
            /*
                曼哈顿距离相似度算法
                原理：曼哈顿距离的实现，同欧式距离相似，都是用于多维数据空间距离的测度
                范围：[0,1]，同欧式距离一致，值越小，说明距离值越大，相似度越大。
                说明：比欧式距离计算量少，性能相对高。
             */
            case CITYBLOCK:
                return new CityBlockSimilarity(model);
            /*
                欧式距离相似度算法
                原理：利用欧式距离d定义的相似度s，s=1 / (1+d)。
                说明：同皮尔森相似度一样，该相似度也没有考虑重叠数对结果的影响。同样，Mahout通过增加一个枚举类型（Weighting）的参数来使得重叠数也成为计算相似度的影响因子。
             */
            case EUCLIDEAN:
            default:
                return new EuclideanDistanceSimilarity(model);
        }
    }

    /**
     * 两个集群之间相似度算法接口
     *
     * @param type {@link SIMILARITY} 相似度算法类型
     * @param us   {@link UserSimilarity} 用户相似性度量
     * @return {@link ClusterSimilarity}
     * @throws TasteException Taste 引擎抛出的异常
     */
    public static ClusterSimilarity clusterSimilarity(SIMILARITY type, UserSimilarity us) throws TasteException {
        switch (type) {
            case NEAREST_NEIGHBOR_CLUSTER:
                return new NearestNeighborClusterSimilarity(us);
            case FARTHEST_NEIGHBOR_CLUSTER:
            default:
                return new FarthestNeighborClusterSimilarity(us);
        }
    }

    /**
     * 领域算法类型枚举
     */
    public enum NEIGHBORHOOD {
        NEAREST, THRESHOLD
    }

    /**
     * 领域算法接口
     *
     * @param type           {@link NEIGHBORHOOD} 相似度算法类型
     * @param userSimilarity {@link UserSimilarity} 用户相似性度量
     * @param model          {@link DataModel} 数据模型
     * @param num            数据模型中的最大用户数量/相似度阈值
     * @return {@link UserNeighborhood}
     * @throws TasteException Taste 引擎抛出的异常
     */
    public static UserNeighborhood userNeighborhood(NEIGHBORHOOD type, UserSimilarity userSimilarity, DataModel model, double num) throws TasteException {
        switch (type) {
            // 规定固定数量，根据相似度值排名，取前固定数量作为一个邻域
            case NEAREST:
                return new NearestNUserNeighborhood((int) num, userSimilarity, model);
            // 规定一个相似阔值，大于这个相似阔值作为一个邻域
            case THRESHOLD:
            default:
                return new ThresholdUserNeighborhood(num, userSimilarity, model);
        }
    }

    /**
     * 用户推荐引擎
     *
     * @param userSimilarity   {@link UserSimilarity} 用户相似性度量
     * @param userNeighborhood {@link UserNeighborhood} 用户领域
     * @param pref             是否有偏好，true：生成基于用户的推荐引擎/false：生成基于用户的无偏好值推荐引擎
     * @return {@link RecommenderBuilder} 用户推荐引擎
     */
    public static RecommenderBuilder userRecommender(final UserSimilarity userSimilarity, final UserNeighborhood userNeighborhood, boolean pref) {
        return pref ? model -> new GenericUserBasedRecommender(model, userNeighborhood, userSimilarity) : model -> new GenericBooleanPrefUserBasedRecommender(model, userNeighborhood, userSimilarity);
    }

    /**
     * 项目推荐引擎
     *
     * @param itemSimilarity {@link ItemSimilarity} 项目相似性度量
     * @param pref           是否有偏好，true：生成基于项目的推荐引擎/false：生成基于项目的无偏好值推荐引擎
     * @return {@link RecommenderBuilder} 用户推荐引擎
     */
    public static RecommenderBuilder itemRecommender(final ItemSimilarity itemSimilarity, boolean pref) {
        return pref ? model -> new GenericItemBasedRecommender(model, itemSimilarity) : model -> new GenericBooleanPrefItemBasedRecommender(model, itemSimilarity);
    }

    public static RecommenderBuilder slopeOneRecommender() {
        return dataModel -> new SlopeOneRecommender(dataModel);
    }

    public static RecommenderBuilder itemKNNRecommender(final ItemSimilarity is, final Optimizer op, final int n) {
        return dataModel -> new KnnItemBasedRecommender(dataModel, is, op, n);
    }

    public static RecommenderBuilder svdRecommender(final Factorizer factorizer) {
        return dataModel -> new SVDRecommender(dataModel, factorizer);
    }

    public static RecommenderBuilder treeClusterRecommender(final ClusterSimilarity cs, final int n) {
        return dataModel -> new TreeClusteringRecommender(dataModel, cs, n);
    }

    /**
     * 打印推荐相关内容
     *
     * @param uid             推荐目标id
     * @param recommendations {@link RecommendedItem} 推荐内容
     * @param skip            是否跳过
     */
    public static void showItems(long uid, List<RecommendedItem> recommendations, boolean skip) {
        if (!skip || recommendations.size() > 0) {
            System.out.printf("uid:%s,", uid);
            for (RecommendedItem recommendation : recommendations) {
                System.out.printf("(%s,%f)", recommendation.getItemID(), recommendation.getValue());
            }
        }
    }

    /**
     * evaluator
     */
    public enum EVALUATOR {
        AVERAGE_ABSOLUTE_DIFFERENCE, RMS
    }

    public static RecommenderEvaluator buildEvaluator(EVALUATOR type) {
        switch (type) {
            case RMS:
                return new RMSRecommenderEvaluator();
            case AVERAGE_ABSOLUTE_DIFFERENCE:
            default:
                return new AverageAbsoluteDifferenceRecommenderEvaluator();
        }
    }

    public static void evaluate(EVALUATOR type, RecommenderBuilder rb, DataModelBuilder mb, DataModel dm, double trainPt) throws TasteException {
        System.out.printf("%s Evaluater Score:%s\n", type.toString(), buildEvaluator(type).evaluate(rb, mb, dm, trainPt, 1.0));
    }

    public static void evaluate(RecommenderEvaluator re, RecommenderBuilder rb, DataModelBuilder mb, DataModel dm, double trainPt) throws TasteException {
        System.out.printf("Evaluater Score:%s\n", re.evaluate(rb, mb, dm, trainPt, 1.0));
    }

    /**
     * statsEvaluator
     */
    public static void statsEvaluator(RecommenderBuilder rb, DataModelBuilder mb, DataModel m, int topn) throws TasteException {
        RecommenderIRStatsEvaluator evaluator = new GenericRecommenderIRStatsEvaluator();
        IRStatistics stats = evaluator.evaluate(rb, mb, m, null, topn, GenericRecommenderIRStatsEvaluator.CHOOSE_THRESHOLD, 1.0);
        // System.out.printf("Recommender IR Evaluator: %s\n", stats);
        System.out.printf("Recommender IR Evaluator: [Precision:%s,Recall:%s]\n", stats.getPrecision(), stats.getRecall());
    }
}
